/*
 Hermes: General Porpouse Mobile Agent Platform
 Copyright (C) 2004 Diego Bonura, Rosario Culmone, Davide Lorenzoli, 
 Leonardo Mariani, Emanuela Merelli, Francesca Piersigilli.

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation; either version 2
 of the License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 Emanuela Merelli
 Dipartimento di Matematica ed Informatica
 via Madonna delle Carceri, 62032 Camerino, Italy
 E.Mail: emanuela.merelli@unicam.it
 */
package hermesV3.services;

import hermesV3.Message;
import hermesV3.Place;
import hermesV3.PlaceAddress;
import hermesV3.agent.Identifier;
import hermesV3.agent.InteractionContainer;
import hermesV3.agent.ServiceAgent;
import hermesV3.protocols.SynProtocol;
import hermesV3.services.HubService.PeerAgent;
import hermesV3.services.HubService.PeeringHandler;
import hermesV3.services.Space.SpaceManager;
import hermesV3.util.HermesLogger;
import it.unicam.cs.cosy.bioshape.core.components.GlobalCoordinator;
import it.unicam.cs.cosy.bioshape.core.components.LocalCoordinator;
import it.unicam.cs.cosy.bioshape.core.util.Messages;
import it.unicam.cs.cosy.bioshape.core.util.XmlReader;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.Properties;

/**
 * Started in each {@link Place}. <br>
 * Depending on its IP and the IP of the GC, it can:
 * <ul>
 * <li>launch the GeneralCoordinator, the {@link SpaceManager} and a
 * {@link PeerAgent}, then it can wait for the {@code PeerAgent} agents requests
 * (including requests from the one it created); for each request received it
 * launches a {@link PeeringHandler};</li>
 * <li>launch a {@link PeerAgent}, which will communicate with the
 * {@code GeneralCoordinator} to become a {@code LocalCoordinator}.</li>
 * </ul>
 * <br>
 * Note that each {@code PeeringHandler} acts on the behalf of the
 * {@code HubAgent} so that also its logs are shown as {@code HubAgent} logs.
 * 
 * @see LocalCoordinator
 * @see GlobalCoordinator
 * @see SpaceManager
 * @see PeeringHandler
 * @see PeerAgent
 * 
 * @author Federico Buti, Gaston Alanis, Matteo Micheletti, Andrea Piermarteri
 */
public class HubAgent extends ServiceAgent {

	private static final long serialVersionUID = 8741270130948648505L;
	private static String propertyFileName = File.separator + "conf"
			+ File.separator + "hermes.properties";
	private static Properties netServiceAgentProperty;
	private int retries;
	private Identifier globalId;
	private Identifier spaceId;
	private String xmlInputFilePath;
	private XmlReader xmlReader;
	private int timeout;
	private Identifier guiId;

	/**
	 * An {@link HubAgent} whit a name.
	 * 
	 * @param name
	 *            this {@link HubAgent} name
	 */
	public HubAgent(String name) {
		super(name);
	}

	/** Main loop of HUB agent {@link Place}. */
	@Override
	public void init() {
		reception();
		// Service registration
		String[] features = new String[2];
		features[0] = "HUB Service";
		features[1] = "HUB";
		registerService(features);
		// protocol registration
		registerProtocol(new SynProtocol());
		HermesLogger.log(HermesLogger.INFO, this.getAgentName()
				+ " - HUB correctly initiated. Waiting for "
				+ "new messages from the user...");
		//
		// Read Properties about Network and BioShape
		readProperties();
		//
		while (true)
			run();
	}

	/**
	 * Continuously executed. Receives requests from the {@code SimulationAgent}
	 * which in turn receives input from the user through the GUI.<br>
	 * It sets up the simulation by generating {@code GlobalCoordinator}/
	 * {@code LocalCoordinator} agents and all the other support agents.
	 * Moreover it launches {@code PeeringHandler} to answer other node
	 * {@code PeerAgent}s willingly to create a simulation environment.
	 */
	private void run() {
		if (!receiveFileInput())
			return;
		if (!parseInput())
			return;
		//TODO Risolvere il problema del lancio di una simulazione da un pc solo
		// non connesso in rete, cioè con IP = 127.0.0.1
		String hubIP = xmlReader.getGlobalIP();
		HermesLogger.log(HermesLogger.DEBUG, this.getAgentName() 
		        + " - Global IP read: " + hubIP);
		// Checking if the place is the GC/LC or solely a LC --> based on IP
		String myIP = getIP();
		HermesLogger.log(HermesLogger.DEBUG, this.getAgentName() 
                + " - My IP : " + myIP);
		if (myIP.equals("127.0.0.1")) {
		    //TODO QUI
			HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
					+ " - An issue occurred while interrogating the NIC. "
					+ "Simulation aborted. Please review you network "
					+ "configuration.");
			Messages.sendAck(this, guiId);
			return;
		}
		//TODO e QUI
		checkCreation(hubIP, myIP);
	}

	/**
	 * Creates {@code GlobalCoordinator} (GC), {@code SpaceManager} (SM) and
	 * {@code LocalCoordinator} (LC) objects accordingly to the IP addresses:
	 * the GC is generated only on the machine which has the same IP assigned to
	 * the {@code GlobalCoordinator} machine. The same rule is applied to the
	 * SM. Instead, the LC is generated on each machine involved in the
	 * simulation.
	 * 
	 * @param hubIP
	 *            {@code String} IP of the {@code HubAgent} machine (used
	 *            input).
	 * @param myIP
	 *            {@code String} IP of the machine the core is running on.
	 */
	private void checkCreation(String hubIP, String myIP) {
		// GLOBAL or PEER?? place branch...
		if (myIP.equalsIgnoreCase(hubIP)) {
			if (!createGCandSM() || !createLC(hubIP)) {
				HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
						+ " - Something Wrong during Coordinators "
						+ "creation. Simulation aborted.");
				Messages.sendAck(this, guiId);
				return;
			}
			// Receiving messages cycle. Any message from GC or the
			// SimulationAgent is seen as a ROLLBACK instruction
			while (true) {
				Message<?> msg = getMessageSynch();
				if (msg.getSenderAgentId().equals(globalId)
						|| msg.getSenderAgentId().equals(guiId)) {
					HermesLogger.log(HermesLogger.INFO, this.getAgentName()
							+ " - resetting for next simulation...");
					return;
				}
				// else managing message...
				HermesLogger.log(HermesLogger.INFO, this.getAgentName()
						+ " - HUB received a message...processing it.");
				new PeeringHandler(this, globalId, spaceId, guiId, msg).start();
			}
		}
		// PEER place branch
		if (!createLC(hubIP)) {
			HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
					+ " - Something Wrong during Coordinators "
					+ "creation. Simulation aborted.");
			Messages.sendAck(this, guiId);
			return;
		}
	}

	/**
	 * Reads the input from the given file.
	 * 
	 * @return {@code true} if the input parsing is successfully terminated
	 *         {@code false} otherwise.
	 */
	private boolean parseInput() {
		try {
			xmlReader = new XmlReader(new File(xmlInputFilePath));
		} catch (Exception ex) {
			Messages.sendAck(this, guiId);
			System.out.println("Exception Validating the xml file: " + ex);
			return false;
		}
		HermesLogger.log(HermesLogger.INFO, this.getAgentName()
				+ " - Parsing XML input...");
		if (xmlReader.parseInput())
			return true;
		Messages.sendAck(this, guiId);
		return false;
	}

	/**
	 * Waits for communications from the {@code SimulationAgent}. Once a message
	 * from the latter arrives, it is checked whether it is an {@code ACK} or a
	 * {@code SYN} message. In the former case it is a reset from
	 * {@code SimulationAgent} and it is safely skipped by the {@code HubAgent}.
	 * In the latter case the {@code SimulationAgent} intend to start a
	 * simulation and the {@code SYN} message contains a reference to the input
	 * file.<br>
	 * The rollback message is necessary to exit the listening loop that a
	 * {@code HubAgent} entered after starting a new simulation (listening fro
	 * {@code PeerAgent}s connection). Typically it is the
	 * {@code GlobalCoordinator} which make the hub exit the loop (once enough
	 * {@code PeerAgent}s has been recognised). Anyway, if something goes wrong
	 * and the {@code GlobalCoordinator} is unable to reset the hub, the message
	 * sent by the {@code SimulationAgent} ensure that the roolback is executed.
	 * 
	 * @return {@code true} if the {@code Message} from the
	 *         {@code SimulationAgent} is correctly received, {@code false}
	 *         otherwise.
	 * @see GlobalCoordinator
	 */
	private boolean receiveFileInput() {
		Message<?> msg;
		do {
			msg = getMessageSynch();
			guiId = msg.getSenderAgentId();
		} while (!guiId.getName().equals("SimulationAgent")
				&& msg.getObject() != null);
		InteractionContainer ic = (InteractionContainer) msg.getObject();
		if (ic.getInteractionName().equals(SynProtocol.INTERACTIONSYN)
				&& ic.validate(new SynProtocol())) {
			xmlInputFilePath = (String) ic.getParameter(SynProtocol.INTERACTIONSYN);
			HermesLogger.log(HermesLogger.INFO, this.getAgentName()
					+ " - Received input file...processing it...");
			return true;
		}
		if (ic.getInteractionName().equals(SynProtocol.INTERACTIONACK)
				&& ic.validate(new SynProtocol()))
			// reset by the SimulationAgent
			return false;
		// MUST NOT BE EXECUTED, EVER, FROM HERE ON. (Messages are always sent
		// correctly!)
		HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
				+ " - Something wrong about the communication with the "
				+ "'SimulationAgent'. Please review the software "
				+ "configuration.");
		Messages.sendAck(this, guiId);
		return false;
	}

	/**
	 * Reads the property set-up file. It contains the number of times the peers
	 * try to contact the {@code HubAgent} and the timeout within which the
	 * {@code HubAgent} replies.
	 */
	private void readProperties() {
		try {
			netServiceAgentProperty = new Properties();
			// I'm going to load my property file
			InputStream fis = new FileInputStream(getPlaceProperties()
					.getProperty("HERMES.HOME_DIRECTORY") + propertyFileName);
			netServiceAgentProperty.load(fis);
		} catch (Exception e) {
			/* NEVER REACHED */
		}
		// Save BioShape properties in the HubAgent state
		retries = Integer.parseInt(netServiceAgentProperty
				.getProperty("BIOSHAPE.PEER_TRIES"));
		timeout = Integer.parseInt(netServiceAgentProperty
				.getProperty("BIOSHAPE.PEER_TIMEOUT"));
	}

	private String getIP() {
		Enumeration<NetworkInterface> interfaces = null;
		InetAddress addr;
		if (System.getProperty("os.name").startsWith("Windows")) {
			try {
				addr = InetAddress.getLocalHost();
				return addr.getHostAddress();
			} catch (UnknownHostException e) {
				HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
						+ " unable to collect IP address...");
				HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
						+ " reading the properties IP address...");
			}
		}
		// Get IP Address the other way around
		try {
			interfaces = NetworkInterface.getNetworkInterfaces();
			while (interfaces.hasMoreElements()) {
				NetworkInterface nif = interfaces.nextElement();
				Enumeration<InetAddress> addresses = nif.getInetAddresses();
				while (addresses.hasMoreElements()) {
					addr = addresses.nextElement();
					if (!addr.isLoopbackAddress()
							&& addr.getClass() == Inet4Address.class) {
						return addr.getHostAddress();
					}
				}
			}
		} catch (SocketException se) {
			HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
					+ " unable to collect IP address...");
		}
		return "127.0.0.1"; // SHOULD NOT REACH THIS CODE...EVER!
	}

	private boolean createLC(String hubIP) {
		String hubport = netServiceAgentProperty.getProperty("HERMESNET.PORT");
		Object[] parameters = { guiId,
				new PlaceAddress(hubIP, Integer.parseInt(hubport)),
				new Integer(retries) };
		Identifier peer = createAgent(PeerAgent.class, parameters);
		return peer == null ? false : true;
	}

	private boolean createGCandSM() {
		// Launches GlobalCoordinator
		globalId = this.createAgent(GlobalCoordinator.class, new Object[] {
				GlobalCoordinator.class.getSimpleName(), xmlReader,
				new Integer(timeout), guiId });
		// Launches SpaceManager
		spaceId = this.createAgent(SpaceManager.class, new Object[] {
				SpaceManager.class.getSimpleName(), xmlReader });
		// Sending ID of the SpaceManager to the GlobalCoordinator
		if (!Messages.sendSyn(this, globalId, spaceId)) {
			HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
					+ "- SpaceID Sending to "
					+ globalId.getBornPlaceAddress().getIp() + " failed...\n");
			HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
					+ " - simulation stopped unexpectedly!");
			return false;
		}
		// Sending ID of the GlobalCoordinator to the SpaceManager
		if (!Messages.sendSyn(this, spaceId, globalId)) {
			HermesLogger.log(HermesLogger.ERROR, this.getAgentName()
					+ "GlobalID Sending to "
					+ spaceId.getBornPlaceAddress().getIp() + " failed...\n"
					+ this.getAgentName()
					+ " - simulation stopped unexpectedly!");
			return false;
		}
		// send Ids to SimulationAgent
		Messages.sendSynAck(this, guiId, new Identifier[] { globalId,
				spaceId });
		return true;
	}
}